package cn.thornbird.orgsync.helper;

import cn.thornbird.orgsync.entity.status.AppType;
import cn.thornbird.orgsync.helper.converter.NeteaseConverter;
import cn.thornbird.orgsync.model.AppUserData;
import cn.thornbird.orgsync.model.TreeData;
import cn.thornbird.orgsync.model.netease.*;
import cn.thornbird.orgsync.util.JsonUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 网易企业邮箱对外开放平台技术白皮书V1.9.6.pdf
 */
public class NeteaseHelper extends AppHelper<AccountList.Account> {

    public static final String API_HOST = "https://api.qiye.163.com";

    public static final String ACQUIRE_TOKEN_PATH = "/api/pub/token/acquireToken";

    public static final String GET_DOMAIN_PATH = "/api/open/domain/getDomain";

    public static final String GET_UNIT_LIST_PATH = "/api/open/unit/getUnitList";

    public static final String GET_SUB_UNIT_LIST_PATH = "/api/open/unit/getSubUnitList";

    private static final String GET_ACCOUNT_LIST_PATH = "/api/open/unit/getAccountList";

    public static final String GET_ACCOUNT_PATH = "/api/open/account/getAccount";

    public static final String CREATE_UNIT_PATH = "/api/open/unit/createUnit";

    public static final String UPDATE_UNIT_PATH = "/api/open/unit/updateUnit";

    public static final String UPDATE_UNIT_RANK_PATH = "/api/open/unit/updateUnitRank";

    public static final String MOVE_UNIT_PATH = "/api/open/unit/moveUnit";

    public static final String DELETE_UNIT_PATH = "/api/open/unit/deleteUnit";

    public static final String CREATE_ACCOUNT_PATH = "/api/open/account/createAccount";

    public static final String UPDATE_ACCOUNT_PATH = "/api/open/account/updateAccount";

    public static final String UPDATE_PASSWORD_PATH = "/api/open/account/updatePassword";

    public static final String MOVE_ACCOUNT_UNIT_PATH = "/api/open/account/moveUnit";

    public static final String DELETE_ACCOUNT_PATH = "/api/open/account/deleteAccount";

    public static final String SOFT_DELETE_ACCOUNT_PATH = "/api/open/account/deleteAccountSim";

    public static final String SUSPEND_ACCOUNT_PATH = "/api/open/account/suspendAccount";

    public static final String RECOVER_ACCOUNT_PATH = "/api/open/account/recoverAccount";

    private static final char[] DIGITS_UPPER = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    };

    private static final Random RANDOM_INSTANCE = new Random();

    private String domain;

    private String appId;

    private String orgOpenId;

    @Override
    public void initialize(RedisTemplate<String, String> redisTemplate, String certificateText) {
        super.initialize(redisTemplate, certificateText);
        Map<String, Object> map = JsonUtil.parseMap(certificateText);
        this.domain = map.get("domain").toString();
        this.appId = map.get("appId").toString();
        this.orgOpenId = map.get("orgOpenId").toString();
    }

    @Override
    public String getToken() {
        return new AccessTokenFactory().getAccessToken(AppType.Netease, certificateText).getToken(redisTemplate);
    }

    @Override
    public List<TreeData> getOrganizationTree() {
        List<TreeData> treeDataList = new ArrayList<>();
        DomainDetail domainDetail = getDomain();
        if (!domainDetail.isSuccess()) {
            return treeDataList;
        }

        TreeData item = new TreeData();
        item.setKey("0");
        item.setTitle(domainDetail.getData().getOrgName());
        treeDataList.add(item);
        UnitList unitList = getUnitList(null);
        if (unitList.isSuccess()) {
            List<UnitList.UnitItem> unitItems = unitList.getData().stream().sorted(
                    Comparator.comparing(UnitList.UnitItem::getUnitParentId,
                            Comparator.nullsFirst(Comparator.naturalOrder())).thenComparing(
                                    UnitList.UnitItem::getRank).reversed()
            ).collect(Collectors.toList());
            generateOrganizationTree(treeDataList, unitItems, item);
        }
        return treeDataList;
    }

    @Override
    public List<AppUserData> getUsersByDepartmentId(String departmentId) {
        List<AppUserData> users = new ArrayList<>();
        AccountList accountList = getAccountList(departmentId, false);
        if (accountList.isSuccess() && accountList.getData().getList() != null) {
            NeteaseConverter converter = new NeteaseConverter();
            for (AccountList.Account account : accountList.getData().getList()) {
                users.add(converter.parseUser(account));
            }
        }
        return users;
    }

    @Override
    public AppUserData findAppUser(AppUserData sourceUser) {
        AppUserData targetUser = null;
        AccountList.Account account = getAccount(UserAttr.EMAIL, sourceUser.getEmail());
        if (account == null) {
            account = getAccount(UserAttr.EMPLOYEE_NO, sourceUser.getEmployeeNo());
        }
        if (account == null) {
            account = getAccount(UserAttr.MOBILE, sourceUser.getMobile());
        }
        if (account == null) {
            account = getAccount(UserAttr.NAME, sourceUser.getName());
        }
        if (account != null) {
            targetUser = new NeteaseConverter().parseUser(account);
        }
        return targetUser;
    }

    @Override
    public AppUserData findAppUserById(String id) {
        AppUserData targetUser = null;
        String email = String.format("%s@%s", id, domain);
        AccountList.Account account = getAccount(UserAttr.EMAIL, email);
        if (account != null) {
            targetUser = new NeteaseConverter().parseUser(account);
        }
        return targetUser;
    }

    public DomainDetail getDomain() {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        return doPost(GET_DOMAIN_PATH, params, token, DomainDetail.class);
    }

    public UnitList getUnitList(String unitId) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        if (StringUtils.isEmpty(unitId)) {
            return doPost(GET_UNIT_LIST_PATH, params, token, UnitList.class);
        }

        params.put("unitId", unitId);
        params.put("recursion", true);
        return doPost(GET_SUB_UNIT_LIST_PATH, params, token, UnitList.class);
    }

    public List<String> getUnitIds(String unitId, UnitList unitList) {
        List<String> unitIds = new ArrayList<>();
        unitIds.add(unitId);
        if (unitList.isSuccess() && unitList.getData() != null) {
            unitIds = unitList.getData().stream().map(UnitList.UnitItem::getUnitId).collect(Collectors.toList());
            unitIds.add(0, unitId);
        }
        return unitIds;
    }

    public AccountList getAccountList(String unitId, boolean recursion) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        int index = 1;
        params.put("domain", domain);
        params.put("pageNum", index);
        params.put("pageSize", 2000);
        params.put("unitId", unitId);
        params.put("recursion", recursion);
        AccountList accountList = null;
        while (true) {
            AccountList result = doPost(GET_ACCOUNT_LIST_PATH, params, token, AccountList.class);
            if (!result.isSuccess()) {
                accountList = result;
                break;
            } else if (accountList == null) {
                accountList = result;
                accountList.getData().setPageNum(1);
                accountList.getData().setPageSize(Integer.MAX_VALUE);
            }
            if (result.getData().getList() == null || result.getData().getList().size() == 0) {
                break;
            }
            if (index > 1) {
                accountList.getData().getList().addAll(result.getData().getList());
            }
            index++;
            params.put("pageNum", index);
        }
        if (accountList.isSuccess()) {
            accountList.getData().setCount(accountList.getData().getList().size());
        }
        return accountList;
    }

    public AccountList.Account getAccount(UserAttr userAttr, Object value) {
        AccountList.Account account = null;
        if (userAttr == null || value == null || StringUtils.isEmpty(value.toString())) {
            return account;
        }

        if (userAttr == UserAttr.EMAIL) {
            String email = value.toString();
            String accountName = email;
            String currentDomain = domain;
            if (email.contains("@")) {
                accountName = email.substring(0, email.indexOf("@"));
                currentDomain = email.substring(email.indexOf("@") + 1);
            }
            Map<String, Object> params = new HashMap<>();
            params.put("accountName", accountName);
            params.put("domain", currentDomain);
            AccountDetail accountDetail = doPost(GET_ACCOUNT_PATH, params, getToken(), AccountDetail.class);
            if (accountDetail.isSuccess() && accountDetail.getData() != null) {
                account = accountDetail.getData();
                return account;
            }
        }

        List<AccountList.Account> users = null;
        if (allUsers == null) {
            AccountList accountList = getAccountList(null, true);
            if (accountList.isSuccess() && accountList.getData() != null) {
                users = accountList.getData().getList();
            }
        } else {
            users = allUsers;
        }
        account = findUser(users, userAttr, value, userAttr == UserAttr.NAME);
        return account;
    }

    public UnitItemData createUnit(String parentId, String unitName, String unitDesc) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        params.put("unitName", unitName);
        if (!StringUtils.isEmpty(parentId)) {
            params.put("parentId", parentId);
        }
        if (!StringUtils.isEmpty(unitDesc)) {
            params.put("unitDesc", unitDesc);
        }
        return doPost(CREATE_UNIT_PATH, params, token, UnitItemData.class);
    }

    public Response updateUnit(String unitId, String unitName, String unitDesc) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        params.put("unitId", unitId);
        params.put("unitName", unitName);
        params.put("unitDesc", unitDesc);
        return doPost(UPDATE_UNIT_PATH, params, token, Response.class);
    }

    public Response updateUnitRank(String unitId, Long rank) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        params.put("unitId", unitId);
        params.put("rank", rank);
        return doPost(UPDATE_UNIT_RANK_PATH, params, token, Response.class);
    }

    public UnitItemData moveUnit(String unitId, String parentId) {
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        params.put("unitId", unitId);
        params.put("unitParentId", StringUtils.isEmpty(parentId) ? "root" : parentId);
        return doPost(MOVE_UNIT_PATH, params, token, UnitItemData.class);
    }

    public Response deleteUnit(String id) {
        UnitList unitList = getUnitList(id);
        List<String> unitIds = getUnitIds(id, unitList);
        AccountList accountList = getAccountList(id, true);
        if (accountList != null && accountList.getData() != null && accountList.getData().getList() != null) {
            for (AccountList.Account account : accountList.getData().getList()) {
                List<String> currentUnitIds = new ArrayList<>();
                List<String> accountUnitIds = account.getUnitList();
                if (accountUnitIds != null) {
                    for (String unitId: accountUnitIds) {
                        if (!unitIds.contains(unitId)) {
                            currentUnitIds.add(unitId);
                        }
                    }
                }
                if (currentUnitIds.isEmpty()) {
                    currentUnitIds.add("default");
                }
                AccountInfo accountInfo = new AccountInfo();
                accountInfo.setDomain(account.getDomain());
                accountInfo.setAccountName(account.getAccountName());
                accountInfo.setUnitId(String.join(",", currentUnitIds));
                moveAccountUnit(accountInfo);
            }
        }
        if (unitList.isSuccess() && unitList.getData() != null) {
            deleteSubunits(unitList, id, id);
        }
        String token = getToken();
        Map<String, Object> params = new HashMap<>();
        params.put("domain", domain);
        params.put("unitId", id);
        return doPost(DELETE_UNIT_PATH, params, token, UnitItemData.class);
    }

    public AccountDetail createAccount(AccountInfo accountInfo) {
        String token = getToken();
        Map<String, Object> params = addDomainToParams(accountInfo.toCreateParams());
        return doPost(CREATE_ACCOUNT_PATH, params, token, AccountDetail.class);
    }

    public Response updateAccount(AccountInfo accountInfo) {
        String token = getToken();
        Map<String, Object> params = addDomainToParams(accountInfo.toUpdateParams());
        return doPost(UPDATE_ACCOUNT_PATH, params, token, Response.class);
    }

    public Response updatePassword(AccountInfo accountInfo) {
        String token = getToken();
        Map<String, Object> params = addDomainToParams(accountInfo.toUpdatePasswordParams());
        return doPost(UPDATE_PASSWORD_PATH, params, token, Response.class);
    }

    public Response moveAccountUnit(AccountInfo accountInfo) {
        String token = getToken();
        Map<String, Object> params = addDomainToParams(accountInfo.toMoveUnitParams());
        return doPost(MOVE_ACCOUNT_UNIT_PATH, params, token, Response.class);
    }

    public Response deleteAccount(String accountName) {
        String token = getToken();
        Map<String, Object> params = getAccountParams(accountName);
        return doPost(DELETE_ACCOUNT_PATH, params, token, Response.class);
    }

    public Response softDeleteAccount(String accountName) {
        String token = getToken();
        Map<String, Object> params = getAccountParams(accountName);
        return doPost(SOFT_DELETE_ACCOUNT_PATH, params, token, Response.class);
    }

    public Response suspendAccount(String accountName) {
        String token = getToken();
        Map<String, Object> params = getAccountParams(accountName);
        return doPost(SUSPEND_ACCOUNT_PATH, params, token, Response.class);
    }

    public Response recoverAccount(String accountName) {
        String token = getToken();
        Map<String, Object> params = getAccountParams(accountName);
        return doPost(RECOVER_ACCOUNT_PATH, params, token, Response.class);
    }

    public <T> T doPost(String uri, Map<String, Object> params, String token, Class<T> clazz) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.add(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8.toString());
        headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        headers.add("qiye-access-token", token);
        headers.add("qiye-org-open-id", orgOpenId);
        headers.add("qiye-app-id", appId);
        headers.add("qiye-timestamp", Long.toString(System.currentTimeMillis()));
        headers.add("qiye-nonce", generateNonce());
        return doPost(API_HOST + uri, headers, params, clazz);
    }

    @Override
    protected boolean checkUser(AccountList.Account user, UserAttr userAttr, Object value) {
        boolean result = false;
        switch (userAttr) {
            case ID:
                if (user.getAccountOpenId().equals(value)) {
                    result = true;
                }
                break;
            case EMPLOYEE_NO:
                if (user.getJobNumber() != null && user.getJobNumber().equals(value)) {
                    result = true;
                }
                break;
            case NAME:
                if (user.getName().equalsIgnoreCase(value.toString())) {
                    result = true;
                }
                break;
            case MOBILE:
                result = compareMobile(user.getMobile(), (String) value);
                break;
            case EMAIL:
            case ENTERPRISE_EMAIL:
                if (user.getAccountName() != null && user.getDomain() != null) {
                    String email = String.format("%s@%s", user.getAccountName(), user.getDomain());
                    if (email.equalsIgnoreCase(value.toString())) {
                        result = true;
                    }
                }
                break;
            default:
                break;
        }
        return result;
    }

    private void generateOrganizationTree(List<TreeData> treeDataList, List<UnitList.UnitItem> unitItems,
                                          TreeData parent) {
        for (int i = 0, count = unitItems.size(); i < count; i++) {
            UnitList.UnitItem unitItem = unitItems.get(i);
            if (unitItem.getUnitParentId() == null) {
                unitItem.setUnitParentId("0");
            }
            if (unitItem.getUnitParentId().equals(parent.getKey())) {
                TreeData item = new TreeData();
                item.setKey(unitItem.getUnitId());
                item.setTitle(unitItem.getUnitName());
                if (parent.getChildren() == null) {
                    parent.setChildren(new ArrayList<>());
                }
                parent.getChildren().add(item);
                generateOrganizationTree(treeDataList, unitItems, item);
            }
        }
    }

    private void deleteSubunits(UnitList unitList, String parentId, String rootId) {
        List<String> ids = new ArrayList<>();
        for (UnitList.UnitItem unitItem : unitList.getData()) {
            if (unitItem.getUnitParentId() != null && unitItem.getUnitParentId().equals(parentId)) {
                ids.add(unitItem.getUnitId());
            }
        }
        for (String id : ids) {
            deleteSubunits(unitList, id, rootId);
        }
        if (!parentId.equals(rootId)) {
            String token = getToken();
            Map<String, Object> params = new HashMap<>();
            params.put("domain", domain);
            params.put("unitId", parentId);
            doPost(DELETE_UNIT_PATH, params, token, UnitItemData.class);
        }
    }

    private Map<String, Object> getAccountParams(String accountName) {
        Map<String, Object> params = new HashMap<>();
        params.put("accountName", accountName);
        params.put("domain", domain);
        return params;
    }

    private Map<String, Object> addDomainToParams(Map<String, Object> params) {
        if (!params.containsKey("domain") || params.get("domain") == null ||
                StringUtils.isEmpty(params.get("domain").toString())) {
            params.put("domain", domain);
        }
        return params;
    }

    private String generateNonce() {
        byte[] content = new byte[6];
        RANDOM_INSTANCE.nextBytes(content);
        final int l = content.length;
        final char[] out = new char[l << 1];
        char[] toDigits = DIGITS_UPPER;
        for (int i = 0, j = 0; i < l; i++) {
            out[j++] = toDigits[(0xF0 & content[i]) >>> 4];
            out[j++] = toDigits[0x0F & content[i]];
        }
        return new String(out);
    }

}
